<?php

namespace app\library;

use ArrayAccess;
use InvalidArgumentException;
use ReflectionClass;
use ReflectionProperty;

/**
 * 结构工厂
 */
abstract class StructFactory implements ArrayAccess
{
    var array $data = [];

    final public function __construct()
    {

    }

    /**
     * 通过数组实例化一个对象, 会检测属性required
     * @param array $data
     * @return $this
     */
    final static public function instance(array $data): StructFactory
    {
        $instance = new static();
        // 反射
        $reflectionClass = new ReflectionClass($instance);
        // 获取所有属性
        $allProperties = $reflectionClass->getProperties();
        // 检测必选属性
        $requiredProperties = $instance->getRequiredProperties($allProperties);

        foreach ($requiredProperties as $requiredProperty) {
            $propertyName = $requiredProperty->getName();
            if (!array_key_exists($propertyName, $data)) {
                throw new InvalidArgumentException("Property '$propertyName' is required");
            }
        }

        foreach ($data as $key => $value) {
            $instance->setProperty($reflectionClass, $key, $value);
        }

        return $instance;
    }

    /**
     * 获取必选属性
     * @param ReflectionProperty[] $allProperties
     * @return ReflectionProperty[]
     */
    final protected function getRequiredProperties(array $allProperties): array
    {
        static $cachedProperties = null;

        if ($cachedProperties === null) {
            $requiredProperties = [];
            //$pattern = '/@required\s+(\w+)/';

            foreach ($allProperties as $property) {
                $docComments = $property->getDocComment();
                preg_match_all('/@required\s+/', $docComments, $matches);
                //var_export($matches);
                if (!empty($matches[0])) {
                    $requiredProperties[] = $property;
                }
            }

            $cachedProperties = $requiredProperties;
        }

        return $cachedProperties;
    }

    /**
     * 将对象转换为关联数组
     * @return array
     */
    final public function toArray(): array
    {
        $reflectionClass = new ReflectionClass($this);
        $properties = $reflectionClass->getProperties();
        $data = [];

        foreach ($properties as $property) {
            if ($property->getName() == 'data') continue;
            $property->setAccessible(true);
            if ($property->isInitialized($this)) {
                $value = $property->getValue($this);
                if (!is_null($value)) {
                    $data[$property->getName()] = $property->getValue($this);
                }
            }
            $property->setAccessible(false);
        }

        return $data;
    }

    /**
     * 设置属性的值
     * @param ReflectionClass $reflectionClass
     * @param string $key
     * @param mixed $value
     */
    final protected function setProperty(ReflectionClass $reflectionClass, string $key, $value): void
    {
        if ($reflectionClass->hasProperty($key)) {
            $property = $reflectionClass->getProperty($key);
            $property->setAccessible(true);
            $property->setValue($this, $value);
            $property->setAccessible(false);
            $this->data[$key] = $value;
        } else {
            throw new InvalidArgumentException("Class does not have a property named '$key'");
        }
    }

    /**
     * @param $offset
     * @return bool
     */
    public function offsetExists($offset): bool
    {
        return array_key_exists($offset, $this->toArray());
    }

    public function offsetGet($offset)
    {
        return $this->toArray()[$offset] ?? null;
    }

    public function offsetSet($offset, $value)
    {
        $this->setProperty(new ReflectionClass($this), $offset, $value);
    }

    public function offsetUnset($offset)
    {
        unset($this->data[$offset]);
    }
}
